﻿using System;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Collections.Specialized;
using System.Configuration.Provider;
using Caramel.NHibernate.Membership.Entities;
using System.Security.Cryptography;
using System.Text;

namespace Caramel.NHibernate.Membership
{
	public class NHibernateMembershipProvider : MembershipProvider
	{
		private const MembershipPasswordFormat PASSWORD_FORMAT_DEFAULT = MembershipPasswordFormat.Hashed;
		private const int MAX_INVALID_PASSWORD_ATTEMPTS_DEFAULT = 5;
		private const int PASSWORD_ATTEMPT_WINDOW_DEFAULT = 10;
		private const int MIN_REQUIRED_PASSWORD_LENGTH_DEFAULT = 8;
		private const int MIN_REQUIRED_NONALPHANUMERIC_CHARACTERS_DEFAULT = 1;
		private const int MIN_REQUIRED_NONALPHA_CHARACTERS_DEFAULT = 0;
		private const int MIN_REQUIRED_UPPERCASE_CHARACTERS_DEFAULT = 0;
		private const int MIN_REQUIRED_LOWERCASE_CHARACTERS_DEFAULT = 0;
		private const int MIN_REQUIRED_ALPHA_CHARACTERS_DEFAULT = 0;
		private const int MIN_REQUIRED_NUMERIC_CHARACTERS_DEFAULT = 0;

		private string _applicationName;
		private MembershipPasswordFormat _passwordFormat;
		private bool _enablePasswordRetrieval;
		private bool _enablePasswordReset;
		private bool _requiresQuestionAndAnswer;
		private bool _requiresUniqueEmail;
		private int _maxInvalidPasswordAttempts;
		private int _passwordAttemptWindow;
		private int _minRequiredPasswordLength;
		private int _minRequiredNonalphanumericCharacters;
		private string _passwordStrengthRegularExpression = "";

		public override string ApplicationName
		{
			get
			{
				return _applicationName;
			}
			set
			{
				_applicationName = value;
			}
		}

		public override bool EnablePasswordReset
		{
			get
			{
				return _enablePasswordReset;
			}
		}

		public override bool EnablePasswordRetrieval
		{
			get
			{
				return _enablePasswordRetrieval;
			}
		}

		public override int MaxInvalidPasswordAttempts
		{
			get
			{
				return _maxInvalidPasswordAttempts;
			}
		}

		public override int MinRequiredNonAlphanumericCharacters
		{
			get
			{
				return _minRequiredNonalphanumericCharacters;
			}
		}

		public override int MinRequiredPasswordLength
		{
			get
			{
				return _minRequiredPasswordLength;
			}
		}

		public override int PasswordAttemptWindow
		{
			get
			{
				return _passwordAttemptWindow;
			}
		}

		public override MembershipPasswordFormat PasswordFormat
		{
			get
			{
				return _passwordFormat;
			}
		}

		public override string PasswordStrengthRegularExpression
		{
			get
			{
				return _passwordStrengthRegularExpression;
			}
		}

		public override bool RequiresQuestionAndAnswer
		{
			get
			{
				return _requiresQuestionAndAnswer;
			}
		}

		public override bool RequiresUniqueEmail
		{
			get
			{
				return _requiresUniqueEmail;
			}
		}

		public override void Initialize(string name, NameValueCollection config)
		{
			if (config == null)
			{
				throw new ArgumentNullException("config");
			}

			if ((name == null) || (name.Length < 1))
			{
				name = this.GetType().Name;
			}

			if (string.IsNullOrEmpty(config["description"]))
			{
				config.Remove("description");
				config.Add("description", "TOPS PrimaryRecipient Profile Provider");
			}

			base.Initialize(name, config);

			if ((config["applicationName"] == null) || (config["applicationName"].Trim() == ""))
			{
				_applicationName = System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath;
			}
			else
			{
				_applicationName = config["applicationName"];
			}

			_passwordFormat = SecurityConfigUtil.GetPasswordFormat(config, PASSWORD_FORMAT_DEFAULT);
			_enablePasswordRetrieval = SecurityConfigUtil.GetBoolValue(config, "enablePasswordRetrieval", false);
			_enablePasswordReset = SecurityConfigUtil.GetBoolValue(config, "enablePasswordReset", true);
			_requiresQuestionAndAnswer = SecurityConfigUtil.GetBoolValue(config, "requiresQuestionAndAnswer", false);
			_requiresUniqueEmail = SecurityConfigUtil.GetBoolValue(config, "requiresUniqueEmail", true);
			_maxInvalidPasswordAttempts = SecurityConfigUtil.GetIntValue(config, "maxInvalidPasswordAttempts", MAX_INVALID_PASSWORD_ATTEMPTS_DEFAULT, true, Int32.MaxValue);
			_passwordAttemptWindow = SecurityConfigUtil.GetIntValue(config, "passwordAttemptWindow", PASSWORD_ATTEMPT_WINDOW_DEFAULT, false, Int32.MaxValue);
			_minRequiredPasswordLength = SecurityConfigUtil.GetIntValue(config, "minRequiredPasswordLength", MIN_REQUIRED_PASSWORD_LENGTH_DEFAULT, false, Int32.MaxValue);
			_minRequiredNonalphanumericCharacters = SecurityConfigUtil.GetIntValue(config, "minRequiredNonalphanumericCharacters", MIN_REQUIRED_NONALPHANUMERIC_CHARACTERS_DEFAULT, true, Int32.MaxValue);

			if (config["passwordStrengthRegularExpression"] != null)
			{
				_passwordStrengthRegularExpression = config["passwordStrengthRegularExpression"];
			}

			config.Remove("applicationName");
			config.Remove("enablePasswordRetrieval");
			config.Remove("enablePasswordReset");
			config.Remove("requiresQuestionAndAnswer");
			config.Remove("requiresUniqueEmail");
			config.Remove("maxInvalidPasswordAttempts");
			config.Remove("passwordAttemptWindow");
			config.Remove("passwordFormat");
			config.Remove("name");
			config.Remove("minRequiredPasswordLength");
			config.Remove("minRequiredNonalphanumericCharacters");
			config.Remove("minRequiredNonalphaCharacters");
			config.Remove("minRequiredUppercaseCharacters");
			config.Remove("minRequiredLowercaseCharacters");
			config.Remove("minRequiredAlphaCharacters");
			config.Remove("minRequiredNumericCharacters");
			config.Remove("passwordExpirationWindow");
			config.Remove("minPasswordChangesBeforeReuse");
			config.Remove("allowUsernameInPassword");
			config.Remove("allowFirstNameInPassword");
			config.Remove("allowLastNameInPassword");
			config.Remove("forcePasswordChangeOnFirstLogin");
			config.Remove("passwordStrengthRegularExpression");

			if (config.Count > 0)
			{
				if (!String.IsNullOrEmpty(config.GetKey(0)))
				{
					throw new ProviderException(String.Format("Attribute not recognized '{0}'.", config.GetKey(0)));
				}
			}
		}

		public override bool ChangePassword(string username, string oldPassword, string newPassword)
		{
			//if (!this.CheckPassword(username, oldPassword, true, true))
			//{
			//    return false;
			//}

			//this.ValidatePassword(newPassword, username);

            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, newPassword, false);
            this.OnValidatingPassword(args);

            if (args.Cancel)
            {
                if (args.FailureInformation != null)
                {
                    throw args.FailureInformation;
                }

                throw new ArgumentException("The custom password validation failed.");
            }

			var membership =
				Repository<aspnet_Membership>.All.
					Where(m => m.Application.LoweredApplicationName == this.Name.ToLower()).
					Where(m => m.User.LoweredUserName == username.ToLower()).
					SingleOrDefault();

			if (membership == null)
			{
				return false;
			}

			var salt = this.GenerateSalt();
			var encodedPassword =

			membership.Password = this.EncodePassword(newPassword, this.PasswordFormat, salt);
			membership.PasswordSalt = salt;
			membership.PasswordFormat = (int)this.PasswordFormat;
			membership.LastPasswordChangedDate = SystemTime.Now;

			Repository<aspnet_Membership>.Save(membership);

			return true;
		}

		public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
		{
			throw new NotImplementedException();
		}

		public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
		{
			using (var work = UnitOfWork.Begin())
			{
				var application = this.EnsureApplication();

				var user =
					Repository<aspnet_Users>.
						Save(
							new aspnet_Users()
							{
								UserName = username,
								LoweredUserName = username.ToLower(),
								Application = application
							}
						);

				var salt = this.GenerateSalt();

				var membership =
					Repository<aspnet_Membership>.
						Save(
							new aspnet_Membership()
							{
								Application = application,
								User = user,
								Password = this.EncodePassword(password, this.PasswordFormat, salt),
								PasswordSalt = salt,
								PasswordFormat = (int)this.PasswordFormat,
							}
						);

				work.Commit();
			}

			status = MembershipCreateStatus.Success;

			return
				new MembershipUser(
					this.Name,
					username,
					providerUserKey,
					email,
					passwordQuestion,
					"",
					isApproved,
					false,
					DateTime.Now,
					DateTime.Now,
					DateTime.Now,
					DateTime.Now,
					DateTime.Now
				);
		}

		public override bool DeleteUser(string username, bool deleteAllRelatedData)
		{
			throw new NotImplementedException();
		}

		public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
		{


			//var membership =
			//    Repository<aspnet_Membership>.All.
			//        Where(m => m.Application.LoweredApplicationName == this.Name.ToLower()).
			//        Where(m => m.LoweredEmail == emailToMatch.ToLower()).
			//        Skip(pageIndex * pageSize).
			//        Take(pageSize).
			//        ToList().
			//        Select(
			//            m =>
			//            new MembershipUser(
			//                this.Name,
			//                m.User.UserName,

			throw new NotImplementedException();	
		}

		public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
		{
			throw new NotImplementedException();
		}

		public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
		{
			throw new NotImplementedException();
		}

		public override int GetNumberOfUsersOnline()
		{
			throw new NotImplementedException();
		}

		public override string GetPassword(string username, string answer)
		{
			throw new NotImplementedException();
		}

		public override MembershipUser GetUser(string username, bool userIsOnline)
		{
			using (UnitOfWork.BeginReadOnly())
			{
				var membership =
					Repository<aspnet_Membership>.All.
						Where(m => m.Application.LoweredApplicationName == this.ApplicationName.ToLower()).
						Where(m => m.User.LoweredUserName == username.ToLower()).
						FirstOrDefault();

				if (membership == null)
				{
					return null;
				}

				return
					new MembershipUser(
						this.Name,
						username,
						null,
						membership.Email,
						membership.PasswordQuestion,
						membership.Comment,
						membership.IsApproved,
						membership.IsLockedOut,
						membership.CreateDate,
						membership.LastLoginDate,
						DateTime.Now,
					/*membership.LastActivityDate,*/
						membership.LastPasswordChangedDate,
						DateTime.Now
					);
			}
		}

		public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
		{
			throw new NotImplementedException();
		}

		public override string GetUserNameByEmail(string email)
		{
			throw new NotImplementedException();
		}

		public override string ResetPassword(string username, string answer)
		{
			throw new NotImplementedException();
		}

		public override bool UnlockUser(string userName)
		{
			throw new NotImplementedException();
		}

		public override void UpdateUser(MembershipUser user)
		{
			throw new NotImplementedException();
		}

		public override bool ValidateUser(string username, string password)
		{
			throw new NotImplementedException();
		}

		private string GenerateSalt()
		{
			var buffer = new byte[16];
			new RNGCryptoServiceProvider().GetBytes(buffer);

			return Convert.ToBase64String(buffer);
		}

		private string EncodePassword(string password, MembershipPasswordFormat passwordFormat, string salt)
		{
			if (passwordFormat == MembershipPasswordFormat.Clear)
			{
				return password;
			}

			var input = Encoding.Unicode.GetBytes(password);
			var actualSalt = Convert.FromBase64String(salt);
			var all = new byte[actualSalt.Length + input.Length];
			byte[] output = null;

			Buffer.BlockCopy(actualSalt, 0, all, 0, actualSalt.Length);
			Buffer.BlockCopy(input, 0, all, actualSalt.Length, input.Length);

			if (passwordFormat == MembershipPasswordFormat.Hashed)
			{
				var algorithm = HashAlgorithm.Create(System.Web.Security.Membership.HashAlgorithmType);
				output = algorithm.ComputeHash(all);
			}
			else
			{
				output = this.EncryptPassword(all);
			}

			return Convert.ToBase64String(output);
		}

		private bool CheckPassword(string username, string password, bool updateLastLoginActivityDate, bool failIfNotApproved)
		{
			int status;
			string dbPassword;
			int passwordFormat;
			string salt;
			int failedPasswordAttemptCount;
			bool isApproved = false;
			DateTime lastLoginDate;
			bool isPasswordCorrect = false;

			this.
				GetPasswordWithFormat(
					username,
					true,
					out status,
					out dbPassword,
					out passwordFormat,
					out salt,
					out failedPasswordAttemptCount,
					out isApproved,
					out lastLoginDate
				);

			if (status != 0)
			{
				return false;
			}

			if (!isApproved && failIfNotApproved)
			{
				return false;
			}

			string encodedPassword = this.EncodePassword(password, (MembershipPasswordFormat)passwordFormat, salt);
			isPasswordCorrect = dbPassword.Equals(encodedPassword);

			//Database db = DatabaseFactory.CreateDatabase();
			//DbCommand command = db.GetStoredProcCommand("EmployeeMembership_UpdateUserInfo");
			//db.AddInParameter(command, "@UserName", DbType.String, username);
			//db.AddInParameter(command, "@IsPasswordCorrect", DbType.Boolean, isPasswordCorrect);
			//db.AddInParameter(command, "@UpdateLastLoginActivityDate", DbType.Boolean, updateLastLoginActivityDate);
			//db.AddInParameter(command, "@MaxInvalidPasswordAttempts", DbType.Int32, this.MaxInvalidPasswordAttempts);
			//db.AddInParameter(command, "@PasswordAttemptWindow", DbType.Int32, this.PasswordAttemptWindow);
			//db.AddInParameter(command, "@LastLoginDate", DbType.DateTime, lastLoginDate);
			//db.AddInParameter(command, "@LastActivityDate", DbType.DateTime, lastActivityDate);
			//db.AddInParameter(command, "@CompanyID", DbType.Int32, TOPSContext.Current.Company.ID);
			//db.ExecuteNonQuery(command);

			return isPasswordCorrect;
		}

		private void GetPasswordWithFormat(string username, bool updateLastLoginActivityDate, out int status, out string password, out int passwordFormat, out string passwordSalt, out int failedPasswordAttemptCount, out bool isApproved, out DateTime lastLoginDate)
		{
			status = 0;

			var membership =
				Repository<aspnet_Membership>.All.
					Where(m => m.Application.LoweredApplicationName == this.Name.ToLower()).
					Where(m => m.User.LoweredUserName == username.ToLower()).
					SingleOrDefault();

			if (membership != null)
			{
				if (membership.IsLockedOut)
				{
					status = 99;
				}

				password = membership.Password;
				passwordSalt = membership.PasswordSalt;
				passwordFormat = membership.PasswordFormat;
				failedPasswordAttemptCount = membership.FailedPasswordAnswerAttemptCount;
				isApproved = membership.IsApproved;
				lastLoginDate = membership.LastLoginDate;
			}
			else
			{
				status = 1;
				password = null;
				passwordSalt = null;
				passwordFormat = 0;
				failedPasswordAttemptCount = 0;
				isApproved = false;
				lastLoginDate = DateTime.Now;
			}
		}

		private aspnet_Applications EnsureApplication()
		{
			var application =
				Repository<aspnet_Applications>.All.
					Where(a => a.LoweredApplicationName == this.ApplicationName.ToLower()).
					SingleOrDefault();

			if (application == null)
			{
				application =
					Repository<aspnet_Applications>.
						Save(
							new aspnet_Applications()
							{
								ApplicationName = this.ApplicationName,
								LoweredApplicationName = this.ApplicationName.ToLower(),
								Description = this.Description
							}
						);
			}

			return application;
		}
	}
}
